<?php

/**
 * @name 网页二维码扫码服务支持
 * @author vipkwd <service@vipkwd.com>
 * @link https://github.com/wxy545812093/vipkwd-phputils
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @copyright The PHP-Tools
 */

declare(strict_types=1);

namespace Vipkwd\Utils\ScanLogin;

use \Exception;
use Vipkwd\Utils\Http;
use Vipkwd\Utils\System\File as VKFile;
use Vipkwd\Utils\Image\Qrcode as VKQrcode;
use Vipkwd\Utils\Image\Compress as VKCompress;
use Vipkwd\Utils\Ip as VkIP;
use Vipkwd\Utils\Wx\Mp\User\User as MpUser;
use Vipkwd\Utils\System\Store as VKStore;
use Vipkwd\Utils\Image\Thumb as VkThumb;

class WebLogin
{

    use Traits;

    private $response;
    private $_qrcodeBuilderApi = 'http://qrcode.hosts.run/processApi.php';
    private $_qrcodeBuilderAppId = 123456;
    private $_options = [];

    private $_hookList = [];
    // private $_request;
    // private $_query;
    const QRCODE_DEFAULT_EXPIRES_SECONDS = 1 * 24 * 3600;//二维码默认1天失效

    private static $wxGzhBindQRcodeSaltKey = 'wxGzhBindQRcode';
    private function __construct($options)
    {
        $this->_options = array_merge([
            "web_pusher_url" => '',
            'scan_event' => '',
            'salt_key' => '',
            'appId' => '',
            'appSecret' => '',
            'appName' => ''
        ], $options);
        // $this->_options['scan_event'] = '';
    }

    public function hookFlush(): WebLogin
    {
        $this->_hookList = [];
        return $this;
    }

    public function setHook(string $key, callable $fn): WebLogin
    {
        $hookDefaultKeys = ["unFollow", "followed", "getBusinessOpenId", "setBusinessOpenId", "getUserInfoByOpenId"];
        if (in_array($key, $hookDefaultKeys) && \is_callable($fn)) {
            $this->_hookList[$key] = $fn;
        }
        return $this;
    }


    /**
     * 为业务侧用户生成 微信绑定二维码
     *   -- 场景：当业务系统要实现微信扫码登录系统，则需要先在业务系统中建立：微信用户(openId)与业务用户(userid)的绑定关系 
     * 
     * @param array $businessParmas 业务侧用户身份数据(关联数组)
     * @param boolean $signWithIP 当前登录IP是否参与签名, 解决微信侧 重定向过来
     * 有因IP不对等，验证不通过的问题
     * 
     * @return void
     * 
     */
    public function wxmpBindQRcodeMaker(string $jumpApiPathInfo, string $qrcodeLocalSavePath, array $businessParams = [], int $expireSeconds = self::QRCODE_DEFAULT_EXPIRES_SECONDS, bool $signWithIP = false)
    {
        $params = array_merge($this->query(), $businessParams, ['tts' => time()]);

        //自定义二维码有效期
        $params['ttl'] = $expireSeconds > 0 ? min(self::QRCODE_DEFAULT_EXPIRES_SECONDS, $expireSeconds) : intval(self::QRCODE_DEFAULT_EXPIRES_SECONDS);

        //如果开启IP验签，则：生成二维码 与 扫描二维码 必需在同一公网下;
        $ipSignEnabled = $signWithIP ? VkIP::getClientIp() : '';

        ksort($params);
        $params['bindKey'] = md5(http_build_query($params) . self::$wxGzhBindQRcodeSaltKey . $ipSignEnabled);

        $this->urlQRCodeGenerator($jumpApiPathInfo, $qrcodeLocalSavePath, $params);
    }


    /**
     * 商户后台管理员账号 - 绑定公众号
     */
    public function wxmpBindQRcodeValidate(string $jumpApiPathInfo, array $businessParams = [], bool $signWithIP = false)
    {
        $params = array_merge($this->query(), $businessParams);
        ksort($params);

        $referer = 'weixin';
        // 关闭验证，防止微信侧 重定向过来 因IP不对等，验证不通过的问题
        if (!isset($params['state']) && !isset($params['code'])) {
            $referer = 'business-app';
            $prevBindKey = time();
            if (isset($params['bindKey'])) {
                $prevBindKey = $params['bindKey'];
                unset($params['bindKey']);
            }

            $hash = md5(http_build_query($params) . self::$wxGzhBindQRcodeSaltKey . ($signWithIP ? VkIP::getClientIp() : ''));
            if ($hash != $prevBindKey) {
                return MpUser::followFailView('签名无效[REP01]');
            }

            if (($params['ttl'] + $params['tts']) < time()) {
                return MpUser::followFailView('此码已失效[REP02]');
            }
            unset($prevBindKey, $hash);

            $businessParamsInvalid = false;
            $wxMpBindStore = [];
            foreach (array_keys($businessParams) as $key) {
                //缓存业务侧用户身份标识，供微信重定向过来再次识别身份
                $params[$key] = isset($params[$key]) ? $params[$key] : null;
                if ($params[$key] === null) {
                    $businessParamsInvalid = true;
                    break;
                }
                $wxMpBindStore[$key] = $params[$key];
            }

            if ($businessParamsInvalid) {
                return MpUser::followFailView('身份认证失败[REP03]');
            }
            VKStore::session("wxmp_bind_cache", $wxMpBindStore);

            if (isset($this->_hookList['getBusinessOpenId'])) {

                $instance = MpUser::instance($this->_options['appId'], $this->_options['appSecret']);
                $businessOpenId = call_user_func($this->_hookList['getBusinessOpenId'], $params);
                if ($businessOpenId) {
                    if (false === $instance->isFollow($businessOpenId)) {
                        // 更新业务侧用户 OpenId
                        $this->invokeHook('setBusinessOpenId', '', $params);
                        return MpUser::followFailView('授权失败[REP04]<p>请先搜索关注微信公众号 "' . $this->_options['appName'] . '" </p>');
                    }
                    return MpUser::followSuccessView('绑定成功[RSP01]');
                }
            } else {
                //缺失业务侧Hook指令 
                return MpUser::followFailView('系统异常[REP06]');
            }
        }
        $instance = MpUser::instance($this->_options['appId'], $this->_options['appSecret']);
        $redirect_url = $this->request('url_path') . '?' . http_build_query($businessParams);
        $res = $instance->getOpenId($redirect_url);
        if (array_key_exists('openid', $res)) {
            if (strlen($res['openid']) > 12) {
                //没有关注公众号
                if (false === $instance->isFollow($res['openid'])) {
                    // 更新业务侧用户 OpenId
                    $this->invokeHook('setBusinessOpenId', '', $params);
                    return MpUser::followFailView('授权失败[REP07]<p>请先搜索关注微信公众号 "' . $this->_options['appName'] . '" </p>');
                }
                // 更新业务侧用户 OpenId
                $this->invokeHook('setBusinessOpenId', $res['openid'], $params);
                return MpUser::followSuccessView("绑定成功[RSP02]");
            }
        }
        return MpUser::followFailView("授权失败[REP09]");
    }

    /**
     * 调用钩子
     * 
     * @param string $hookName
     * @param mixed  arg1 参数1
     * @param mixed  arg2 参数2
     * @param mixed  argN 参数n
     * 
     * @return array|null
     */
    private function invokeHook(string $hookName)
    {
        if (isset($this->_hookList[$hookName])) {
            $args = func_get_args();
            unset($args[0]);
            sort($args);
            return call_user_func_array($this->_hookList[$hookName], $args);
        }
        return null;
    }

    /**
     * 下载远程二维码到本地存储
     * 
     * @return string|void
     */
    private function remoteQRCodeImgToLocal(string $localSavePath, array $qrcodeData): string
    {

        if(isset($qrcodeData['errore'])){
            // VKQrcode::make($qrcodeData['errore'], false, 'M', 10);
            VkThumb::instance()->createPlaceholder("200x200",1,1, 14, $qrcodeData['errore']);
        }

        $localSavePath = VKFile::realpath($localSavePath);
        VKFile::createDir($localSavePath);
        $localSavePath = $localSavePath . '/' . $qrcodeData['png'];

        VKFile::write($localSavePath, VKFile::read($qrcodeData['origin'] . '/' . $qrcodeData['placeholder']));
        return $localSavePath;
    }


    /**
     * 生成网址二维码并输出到浏览器
     * 
     * @return void
     */
    private function urlQRCodeGenerator(string $url, string $localSavePath, array $urlParams = [])
    {
        $url = $this->padRelationUrl($url, $urlParams);
        $data = Http::post($this->_qrcodeBuilderApi, [
            'clientId' => $urlParams['clientId'],
            'appId' => $this->_qrcodeBuilderAppId,
            'size' => 4,
            'level' => 'M',
            'link' => $url
        ]);
        $qrcodeLocalFile = $this->remoteQRCodeImgToLocal($localSavePath, $data);

        VKCompress::instance()->setOrigin($qrcodeLocalFile)->compress();
    }


    /**
     * 生成微信登录二维码
     */
    public function loginQRcodeMaker(string $scanValidateApiWithoutArgs, string $qrcodeLocalSavePath, array $data = [], int $expireSeconds = -1)
    {
        $params = ['qr_notice' => false, 'qr_event' => '', 'qr_type' => 'url', 'qr_expires' => 0, 'time' => time()];
        if ($expireSeconds > 0) {
            $params['qr_expires'] = $params['time'] + $expireSeconds;
        }
        $params = array_merge($params, $data);

        $params = $this->loginQrcodeSign(false, $params);

        $this->urlQRCodeGenerator($scanValidateApiWithoutArgs, $qrcodeLocalSavePath, $params);

        // VkQrcode::make($this->padRelationUrl($url, $params), false, '30%');
    }

    /**
     * 微信扫描并验证 二维码
     */
    public function loginQRcodeValidate(string $bingGzhApiUrl)
    {
        $expired_secends = 100 * 60; //10分钟
        $params = $this->loginQrcodeSign(true, $this->query());
        if ($params['sign'] == $params['oldSign']) {

            if (($params['time'] + $expired_secends) > time()) {
                // \Vipkwd\Utils\System\File::writeAppend('open.log', "expired: ok");
                if ($params['btnConfirm']) {
                    // \Vipkwd\Utils\System\File::writeAppend('open.log', "btnConfirm: true");
                    // 数据库验证成功
                    if ($params['dbConfirm']) {

                        // \Vipkwd\Utils\System\File::writeAppend('open.log', "dbConfirm: true");

                        $confirmAuthArgs = explode('-', $params['dbConfirm']);

                        //获取前序记录的授权成功用户身份标识
                        $authArgs = VKStore::session('wxGzh_auth_next_redirect');
                        $status = false;
                        if (is_array($authArgs)) {
                            $status = true;
                            $_authArgs = array_values($authArgs);
                            foreach ($confirmAuthArgs as $arg) {
                                if (!in_array($arg, $_authArgs)) {
                                    $status = false;
                                    break;
                                }
                            }
                        }
                        // \Vipkwd\Utils\System\File::writeAppend('open.log', "authArgs: " . $status ? 1 : 0);
                        if ($status) {
                            //通知页面授权结果
                            $this->pushWebMsg($params['clientId'], $params['qrcodeId'], 'confirm', [
                                'formData' => $authArgs
                            ]);
                            return MpUser::followSuccessView('授权成功，请继续客户端操作!');
                        }

                        return MpUser::wxBrowserMessage('未识别的身份标识', 'error');
                    }

                    // 记住当前url后跳转
                    VKStore::session('wxGzh_bindok_next_redirect', $this->request('path_info'));

                    // 当前方法自有参数阅后即焚 防止影响下一步验签
                    if(isset($params['btnConfirm'])) unset($params['btnConfirm']);
                    if(isset($params['oldSign'])) unset($params['oldSign']);

                    $redirect_url = $this->padRelationUrl( '/' . trim(trim($bingGzhApiUrl, '?'), '/') , [
                        ...$params,
                        'clientId' => $params['clientId'],
                        'qrcodeId' => $params['qrcodeId'],
                        'time' => $params['time'],
                        'sign' => $params['sign'],
                        'confirm' => true,
                        'scanerValidator' => 1
                    ]);
                    // 跳转微信获取OPENID , 并落库验证
                    $this->response()->redirect($redirect_url);
                }
                //通知页面扫码结果
                $this->pushWebMsg($params['clientId'], $params['qrcodeId'], 'complete');

                $message = '<a href="' . $this->request('url_path') . '?' . (http_build_query($this->query())) . '&confirm=true">';
                $message .= '<h4 class="weui_msg_title" style="border: 1px solid #008dff; background: #008dff; color: #fff; padding: 10px; border-radius: 100px; margin-top: 4rem; display: flex; align-items: center; justify-content: center;">确认登录</h4>';
                $message .= '</a>';
                $message .= '<div class="qrcode-copyright">';
                // $message .= '<div>ClientId: ' . $params['clientId'] . '</div>';
                $message .= '<div>QRcodeId: ' . $params['qrcodeId'] . '</div>';
                $message .= '</div>';
                return MpUser::wxBrowserMessage($message, 'success', true);
            }
        }

        return MpUser::wxBrowserMessage("客户端失效", 'safe_warn');
    }


    /**
     * 微信扫描并验证通过，授权登录
     *  -1： 当前微信没有关注公众号，会提示先关注
     *  -2： 业务侧用户未当前微信建立绑定关系，会提示选择绑定
     *  -3： 已关注公众号 且业务侧已建立绑定关系，请在fn回调内 以关联数组返回 业务侧用户身份唯一标识
     * 
     * @param string $appId 公众号应用ID
     * @param string $appKey 公众号应用密钥
     * @param iterable $fn $fn(string: openid):array|false 返回关联数组时建议数组传递业务则的用户唯一标识以供授权完成后socket下发归属通知，如 [userid=>1000]
     * 
     * @return mixed
     */
    public function loginQRcodeBindWxGzh()
    {

        $params = array_merge(['scanerValidator' => ''], $this->query());

        // 获取前序记住的url
        $loginQRcodeValidateApi = VKStore::session('wxGzh_bindok_next_redirect');

        if ($params['scanerValidator'] > 0) {
            VKStore::session('scanerValidator', $params['scanerValidator']);
        } else {
            $params['scanerValidator'] = VKStore::session('scanerValidator', 0);
        }

        if ($params['scanerValidator'] <= 0) {
            return MpUser::followFailView('身份认证失败');
        }
        $instance = MpUser::instance($this->_options['appId'], $this->_options['appSecret']);
        //绑定
        $redirect_url = $this->request('url_path') . '?' . http_build_query($params);
        $res = $instance->getOpenId($redirect_url);
        if (array_key_exists('openid', $res)) {
            if (strlen($res['openid']) > 12) {

                // 当前方法自有参数阅后即焚 防止影响下一步验签
                if(isset($params['code'])) unset($params['code']);
                if(isset($params['state'])) unset($params['state']);
                if(isset($params['scanerValidator'])) unset($params['scanerValidator']);

                //没有关注公众号
                if (false === $instance->isFollow($res['openid'])) {
                    $this->invokeHook('setBusinessOpenId', '', $params);
                    return MpUser::followFailView('授权失败<p>请先搜索关注微信公众号 "' . $this->_options['appName'] . '" </p>');
                }

                $validater = $this->invokeHook('getUserInfoByOpenId', $res['openid'], $params);

                $url = $this->padRelationUrl($loginQRcodeValidateApi, [
                    ...$params,
                    'clientId' => $params['clientId'],
                    'qrcodeId' => $params['qrcodeId'],
                    'time' => $params['time'],
                    'sign' => $params['sign'],
                    'confirm' => true,
                    'dbConfirm' => implode('-', array_values($validater))
                ]);

                // \Vipkwd\Utils\System\File::writeAppend('open.log', [$validater, $url]);
                //存在业务则用户ID
                if (is_array($validater)) {
                    ksort($validater);
                    //记录授权成功的用户身份标识
                    VKStore::session('wxGzh_auth_next_redirect', $validater);
                    // $this->response()->redirect($loginQRcodeValidateApi, [
                    return $this->response()->redirect( $url);
                }
                return MpUser::followFailView('请先在业务系统绑定此微信号');
            }
        }
        return MpUser::followFailView('请在微信或小程序内使用此功能');

    }


    private function loginQrcodeSign(bool $verify = false, array $params = [])
    {
        if (empty($params)) {
            $params = ['clientId' => $this->query('clientId', ''), 'qrcodeId' => $this->query('qrcodeId', ''), 'time' => time()];
        }
        if ($this->query('clientId')) {
            $params['clientId'] = $this->query('clientId');
        }
        if ($this->query('qrcodeId')) {
            $params['qrcodeId'] = $this->query('qrcodeId');
        }
        if ($this->query('xcode')) {
            $params['qrcodeId'] = $this->query('xcode');
        }

        // $params['time'] = time();
        // $params = array_merge(['qr_notice' => false, 'qr_event' => $params['event'] ?? '', 'qr_type' => 'url', 'qr_expires' => 0], $params);

        $oldSign = null;
        $btnConfirm = null;
        $dbConfirm = null;
        if ($verify === true) {
            foreach (array_keys($params) as $field) {
                if ($field == 'sign') {
                    $oldSign = $params['sign'];
                    unset($params['sign']);
                } else if ($field == 'confirm') {
                    $btnConfirm = $params['confirm'];
                    unset($params['confirm']);
                } else if ($field == 'dbConfirm') {
                    $dbConfirm = $params['dbConfirm'];
                    unset($params['dbConfirm']);
                } else {
                    // devdump($params,1);
                    $params[$field] = urldecode($params[$field]);
                }
            }
        }
        ksort($params);
        $params['sign'] = md5(md5(http_build_query($params)) . 'scanLoginImage');
        // echo 'sign: '.http_build_query($params);
        // echo PHP_EOL;
        if ($oldSign) {
            $params['btnConfirm'] = $btnConfirm;
            $params['dbConfirm'] = $dbConfirm;
            $params['oldSign'] = $oldSign;
        }
        // echo "{$params['sign']} == {$params['oldSign']}";
        return $params;
    }

    /**
     * 生成文本二维码并输出到浏览器
     * 
     * @return void
     */
    private function textQRCodeGenerator(string $text, string $savePath)
    {
        $data = Http::post($this->_qrcodeBuilderApi, [
            'appId' => $this->_qrcodeBuilderAppId,
            'size' => 4,
            'level' => 'M',
            'text' => trim($text),
        ]);
        $qrcodeLocalFile = $this->remoteQRCodeImgToLocal($savePath, $data);

        VKCompress::instance()->setOrigin($qrcodeLocalFile)->compress();
    }
    /**
     * URL关联填充
     */
    private function padRelationUrl(string $url, array $params = []): string
    {
        $url = trim(urldecode($url));
        if (substr($url, 0, 4) != 'http') {
            if (substr($url, 0, 2) == '//') {
                $url = $this->request('scheme') . ':' . $url;
            } else {
                $url = $this->request('domain') . '/' . ltrim($url, '/');
            }
        }
        return $url . "?" . http_build_query($params);
    }

}